package org.apache.catalina.core;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;

import org.apache.catalina.core.TenantContext;
import org.apache.catalina.loader.ResourceEntry;
import org.apache.catalina.loader.WebappClassLoader;

/**
 * Our custom implementation of the ClassLoader.
 * For any of classes from "javablogging" package
 * it will use its {@link PerTenantClassLoader#getClass()}
 * method to load it from the specific .class file. For any
 * other class it will use the super.loadClass() method
 * from ClassLoader, which will eventually pass the
 * request to the parent.
 *
 */
public class PerTenantClassLoader extends ClassLoader {
	
	
    byte[] b = null;

    String tenantSensitiveClasses;
    String tenantSensitiveResources;
    String tenantId;
    String metadataRepository; 


    WebappClassLoader webappClassLoader;
    
    //protected HashMap<String, ResourceEntry> resourceEntries = new HashMap<String, ResourceEntry>();
    
    /**
     * Parent ClassLoader passed to this constructor
     * will be used if this ClassLoader can not resolve a
     * particular class.
     *
     * @param parent Parent ClassLoader
     *              (may be from getClass().getClassLoader())
     */
    public PerTenantClassLoader(WebappClassLoader parent, String tenantId, String tenantSensitiveClasses, String tenantSensitiveResources, String metadataRepository) {
        super(parent);
        
        webappClassLoader = parent;
        
        this.tenantSensitiveClasses = tenantSensitiveClasses;
        this.tenantSensitiveResources = tenantSensitiveResources;
        this.metadataRepository = (metadataRepository != null ? metadataRepository : "metadata");
        this.tenantId = tenantId;  
    }

    /**
     * Loads a given class from .class file just like
     * the default ClassLoader. This method could be
     * changed to load the class over network from some
     * other server or from the database.
     *
     * @param name Full class name
     */
    private Class<?> getClass(String name)
        throws ClassNotFoundException {
        // We are getting a name that looks like
        // javablogging.package.ClassToLoad
        // and we have to convert it into the .class file name
        // like javablogging/package/ClassToLoad.class
        String file = name.replace('.', File.separatorChar)
            + ".class";
        try {
            // This loads the byte code data from the file
            b = loadData(file);
            // defineClass is inherited from the ClassLoader class
            // and converts the byte array into a Class
            Class<?> c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {

        return (loadClass(name, false));

    }


    /**
     * Every request for a class passes through this method.
     * If the requested class is in "javablogging" package,
     * it will load it using the
     * {@link PerTenantClassLoader#getClass()} method.
     * If not, it will use the super.loadClass() method
     * which in turn will pass the request to the parent.
     *
     * @param name
     *            Full class name
     */
    @Override
    public Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
        
    	/*
    	if(resourceEntries.containsKey(name)){
    		ResourceEntry entry = resourceEntries.get(name);
            if (entry != null) {
            	System.out.println("loading cached class '" + name + "'");
            	
                return entry.loadedClass;
            }
    	}
    	*/
    	
        // (0.2) Try loading the class with the system class loader, to prevent
        //       the webapp from overriding J2SE classes
    	/*
        try {
            Class clazz = getParent().getSystemClassLoader().loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }


    	boolean isLocalClass = false;
        try {
            Class clazz = webappClassLoader.findClassInternal(name);
            if (clazz != null) {
            	
            	isLocalClass = true;
            }
        }catch(Exception e){
        }
        */
    	
        //isLocalClass = true;
    	System.out.println("name : " + name);
                
        boolean apply = false;
    	if (true && tenantSensitiveClasses!=null){
	    	String[] tenantSensitiveClassesArray = new String[1];
			if(this.tenantSensitiveClasses.indexOf(',') == -1)
				tenantSensitiveClassesArray[0] = this.tenantSensitiveClasses;
			else
				tenantSensitiveClassesArray = this.tenantSensitiveClasses.split(",");
			
			for(String tenantSensitiveClass : tenantSensitiveClassesArray){
				tenantSensitiveClass.trim();
				if(tenantSensitiveClass.endsWith("*")){
					if(name.startsWith(tenantSensitiveClass.substring(0, tenantSensitiveClass.length()-1))){
						apply = true; 
						break;
					}
				}else{
					if(tenantSensitiveClass.equals(name))
						apply = true;
				}
			}
    	}
		
        if (apply) {
            System.out.println("loading class '" + name + "'");
            Class clazz = getClass(name);
            
            /*
            ResourceEntry entry = new ResourceEntry();
            entry.loadedClass = clazz;
            entry.binaryContent = null;
            entry.source = null;
            entry.codeBase = null;
            entry.manifest = null;
            entry.certificates = null;
            
            resourceEntries.put(name, entry);
            */
            
            return clazz;
        }
        
        return webappClassLoader.loadClass(name, resolve);
    }

    /**
     * Loads a given file (presumably .class) into a byte array.
     * The file should be accessible as a resource, for example
     * it could be located on the classpath.
     *
     * @param name File name to load
     * @return Byte array read from the file
     * @throws IOException Is thrown when there
     *               was some problem reading the file
     */
    private byte[] loadData(String name) throws IOException {
        // Opening the file
        InputStream stream = getParent()
            .getResourceAsStream(name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        // Reading the binary data
        in.readFully(buff);
        in.close();
        return buff;
    }

	@Override
	public URL getResource(String name) {
		String tenantId = TenantContext.getCurrentThreadTenantId();
		
		if(tenantSensitiveResources!=null && tenantSensitiveResources.indexOf(name)>-1){
			
			String metadataLocation = metadataRepository + "/" + tenantId + "/" + name;
			File metadataFile = new File(metadataLocation);
			if(metadataFile.exists()){
				try {
					return metadataFile.toURI().toURL();
				} catch (MalformedURLException e) {
					// TODO Auto-generated catch block
					throw new RuntimeException("Enable to load tenant's metadata ["+ metadataLocation +"]", e);
				}
			}else{
			
				InputStream resourceStream = getParent().getResourceAsStream(name);
				
				try {
					byte[] buffer = loadData(name);
					
					String text = new String(buffer);
					text = text.replaceAll("<%=tenantId%>", tenantId);
					
					metadataFile.getParentFile().mkdirs();
					metadataFile.createNewFile();
					FileOutputStream fos = new FileOutputStream(metadataFile);
					
					fos.write(text.getBytes());
					fos.close();
					
					return metadataFile.toURI().toURL();
					
				} catch (IOException e) {
					throw new RuntimeException("Enable to store new tenant-specific metadata file at [" + metadataLocation + "]", e);
				}
			
				
			}
		}
		
		
		return super.getResource(name);
	}

    

}